package com.chudian.service.impl;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chudian.config.WxPayConfig;
import com.chudian.entity.OrderInfo;
import com.chudian.entity.RefundInfo;
import com.chudian.enums.OrderStatus;
import com.chudian.enums.PayType;
import com.chudian.enums.wxpay.WxApiType;
import com.chudian.enums.wxpay.WxNotifyType;
import com.chudian.enums.wxpay.WxRefundStatus;
import com.chudian.enums.wxpay.WxTradeState;
import com.chudian.service.OrderInfoService;
import com.chudian.service.PaymentInfoService;
import com.chudian.service.RefundInfoService;
import com.chudian.service.WxPayService;
import com.chudian.task.WxPayTask;
import com.chudian.util.OrderNoUtils;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import io.swagger.models.parameters.BodyParameter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author : ZhangCheng
 * @date Date : 2022年10月26日
 */
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Autowired
    private CloseableHttpClient wxHttpClient;

    @Autowired
    private CloseableHttpClient wxPayNoSignClient;

    @Autowired
    private WxPayConfig wxPayConfig;

    @Autowired
    private OrderInfoService orderInfoService;

    @Autowired
    private PaymentInfoService paymentInfoService;

    @Autowired
    private RefundInfoService refundInfoService;

    //定义锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception {

        log.info("生成订单");

        //生成订单
        OrderInfo orderInfo = orderInfoService.createOrderInfoByProductId(productId, PayType.WXPAY.getType());
        String code_url = orderInfo.getCodeUrl();
        if(orderInfo != null && !StringUtils.isEmpty(code_url)){
            log.info("订单已存在，二维码已保存！");
            //返回二维码
            HashMap<String, Object> map = new HashMap<>();
            map.put("codeUrl",code_url);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        }

        log.info("调用统一下单api");

        //调用统一下单api
        HttpPost httpPost = new HttpPost(
                wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
        // 请求body参数
        Gson gson=new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("appid",wxPayConfig.getAppid());
        paramsMap.put("mchid",wxPayConfig.getMchId());
        paramsMap.put("description",orderInfo.getTitle());
        paramsMap.put("out_trade_no",orderInfo.getOrderNo());
        paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

        HashMap amountMap = new HashMap();
        amountMap.put("total",orderInfo.getTotalFee());
        amountMap.put("currency","CNY");

        paramsMap.put("amount",amountMap);

        //将参数转换为json字符串
        String paramsJson = gson.toJson(paramsMap);
        log.info("请求参数："+paramsJson);

        StringEntity entity = new StringEntity(paramsJson,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpPost);

        try {
            String responseBody = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//相应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功,返回结果 = " + responseBody);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("失败,返回代码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
                throw new IOException("request failed");
            }

            //响应结果转换为map
            HashMap<String,String> hashMap = gson.fromJson(responseBody, HashMap.class);
            code_url = hashMap.get("code_url");

            //保存二维码
            String orderNo = orderInfo.getOrderNo();
            orderInfoService.saveCodeUrl(orderNo,code_url);

            //返回二维码
            HashMap<String, Object> map = new HashMap<>();
            map.put("codeUrl",code_url);
            map.put("orderNo", orderInfo.getOrderNo());

            return map;
        } finally {
            response.close();
        }
    }

    /**
     * 处理订单
     * @param bodyMap
     * @throws Exception
     */
    @Override
    public void processorOrder(HashMap<String, Object> bodyMap) throws Exception {
        log.info("处理订单");

        //解密报文
        String plainText=decryptFromResource(bodyMap);

        //转换明文
        Gson gson = new Gson();
        HashMap<String,Object> hashMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String) hashMap.get("out_trade_no");
        String orderStatus = (String) hashMap.get("trade_state_desc");

        /*
        在对业务数据进行状态检查和处理之前，
        要采用数据锁进行并发控制，
        以避免函数重入造成的数据混乱
        */
        //尝试获取锁：
        //成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的
                String orderState = orderInfoService.getOrderState(orderNo);
                if(!OrderStatus.NOTPAY.getType().equals(orderState)){
                    //如果当前订单状态不是未支付，则直接返回不再继续处理订单
                    return;
                }

                //模拟通知并发
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //更新订单状态
                orderInfoService.updateOrderStatusByOrderno(orderNo,orderStatus);
                //记录日志数据
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                //要主动的释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 调用微信的查单接口，根据订单号查询订单
     * @param orderNo
     * @return
     */
    @Override
    public String queryOrder(String orderNo) throws Exception {
        log.info("查单接口调用===>{}",orderNo);

        //创建远程请求对象
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
        HttpGet httpGet = new HttpGet(url);

        //设置返回参数
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpGet);

        //处理返回值
        try {
            String responseBody = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//相应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功,返回结果 = " + responseBody);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("Native下单失败,返回码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
                throw new IOException("request failed");
            }
            return responseBody;

        } finally {
            response.close();
        }

    }

    /**
     * 根据订单号查询微信支付查单接口，核实订单状态
     * 如果订单已支付，则更新商户端订单状态，并记录支付日志
     * 如果订单未支付，则调用关单接口关闭订单，并更新商户端订单状态
     * @param orderNo
     */
    @Override
    public void checkOrderStatus(String orderNo) throws Exception {
        log.warn("根据订单号核实订单状态===>{}",orderNo);

        //调用微信支付的查单接口
        String result = this.queryOrder(orderNo);
        Gson gson = new Gson();
        Map resultMap = gson.fromJson(result, HashMap.class);
        //获取微信支付端订单状态
        Object tradeState = resultMap.get("trade_state");

        //判断订单状态
        if(WxTradeState.SUCCESS.getType().equals(tradeState)){
            log.warn("核实订单已支付 ===> {}", orderNo);

            //如果确认订单状态是支付成功，则更新商户端的订单状态
            orderInfoService.updateOrderStatusByOrderno(orderNo, OrderStatus.SUCCESS.getType());

            //记录支付日志
            //由于查单接口返回的result,和从支付通知中的ciperText字段解密后的明文内容一致，所以可以直接作为参数使用
            paymentInfoService.createPaymentInfo(result);
        }

        //判断订单状态
        if(WxTradeState.NOTPAY.getType().equals(tradeState)){
            log.warn("核实订单未支付===>{}",orderNo);

            //如果确认订单未支付，则调用关单接口关闭订单
            this.wxCloseOrder(orderNo);

            //更新商户端订单状态，无须记录日志
            orderInfoService.updateOrderStatusByOrderno(orderNo, OrderStatus.CLOSED.getType());
        }

    }

    /**
     * 退款
     * @param orderNo
     * @param reason
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) throws Exception {

        log.info("创建退款单记录");
        //根据订单号创建退款单
        RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);

        log.info("调用退款api");

        //调用统一下单API
        HttpPost httpPost = new HttpPost(
                wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType()));
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("out_trade_no",orderNo);//订单编号
        paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款单编号
        paramsMap.put("reason",reason);//退款原因
        paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));

        Map amountMap = new HashMap();
        amountMap.put("refund",refundInfo.getRefund());//退款金额
        amountMap.put("total",refundInfo.getTotalFee());//原订单金额
        amountMap.put("currency","CNY");//退款币种
        paramsMap.put("amount",amountMap);

        //将参数转换为json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}",jsonParams);

        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpPost);

        try {
            //解析响应结果
            String responseBody = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//相应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功,退款返回结果 = " + responseBody);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                throw new RuntimeException("退款异常，响应码 = " + statusCode + ",退款返回结果 = " + statusCode);
            }

            //更新订单状态
            orderInfoService.updateOrderStatusByOrderno(orderNo, OrderStatus.REFUND_PROCESSING.getType());

            //更新退款单
            refundInfoService.updateRefund(responseBody);
        } finally {
            response.close();
        }

    }

    /**
     * 查询退款接口调用
     * @param refundNo
     * @return
     */
    @Override
    public String queryRefund(String refundNo) throws Exception {

        log.info("查询退款接口调用 ===> {}", refundNo);

        //组装请求url
        String url = String.format(
                WxApiType.DOMESTIC_REFUNDS_QUERY.getType(),
                refundNo);
        url = wxPayConfig.getDomain().concat(url);

        //创建远程Get请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpGet);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 查询退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

    /**
     * 处理退款单
     * @param bodyMap
     */
    @Override
    public void processorRefund(HashMap<String, Object> bodyMap) throws Exception {
        log.info("处理退款单");

        //解密报文
        String plainText=decryptFromResource(bodyMap);

        //转换明文为map
        Gson gson = new Gson();
        HashMap<String,Object> hashMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String) hashMap.get("out_trade_no");

        /*
        在对业务数据进行状态检查和处理之前，
        要采用数据锁进行并发控制，
        以避免函数重入造成的数据混乱
        */
        //尝试获取锁：
        //成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的
                String orderState = orderInfoService.getOrderState(orderNo);
                if(!OrderStatus.REFUND_PROCESSING.getType().equals(orderState)){
                    //如果当前订单状态不是退款中，则直接返回不再继续处理订单
                    return;
                }

                //更新订单状态
                orderInfoService.updateOrderStatusByOrderno(orderNo,OrderStatus.REFUND_SUCCESS.getType());

                //更新退款单
                refundInfoService.updateRefund(plainText);

            } finally {
                //要主动的释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 根据退款单号核实退款单状态
     * @param refundNo
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkRefundStatus(String refundNo) throws Exception {

        log.info("根据退款单号核实退款单状态===>{}",refundNo);

        //调用微信支付的查询退款单接口
        String result = this.queryRefund(refundNo);

        //将返回结果转换为Map
        Gson gson = new Gson();
        Map<String,String> resultMap = gson.fromJson(result, HashMap.class);

        //获取微信支付端退款状态
        String orderNo = resultMap.get("out_trade_no");
        String orderState = resultMap.get("status");

        //判断订单状态
        if(WxRefundStatus.SUCCESS.getType().equals(orderState)){
            log.warn("核实订单已退款成功 ===> {}", refundNo);
            //更新订单状态
            orderInfoService.updateOrderStatusByOrderno(orderNo, OrderStatus.REFUND_SUCCESS.getType());
            //更新退款单
            refundInfoService.updateRefund(result);
        }

        //判断订单状态
        if (WxRefundStatus.ABNORMAL.getType().equals(orderState)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认退款异常，则更新订单状态
            orderInfoService.updateOrderStatusByOrderno(orderNo, OrderStatus.REFUND_ABNORMAL.getType());
            //更新退款单
            refundInfoService.updateRefund(result);
        }
    }

    /**
     * 申请账单
     * @param billDate
     * @param type
     * @return 账单下载的url
     * @throws Exception
     */
    @Override
    public String queryBill(String billDate, String type) throws Exception {

        log.info("申请账单接口调用===>{}",billDate);

        //根据账单类型，组装请求url
        String url = "";
        if("tradebill".equals(type)){
            url = WxApiType.TRADE_BILLS.getType();
        }else if("fundflowbill".equals(type)){
            url = WxApiType.FUND_FLOW_BILLS.getType();
        }else{
            throw new RuntimeException("抱歉，不支持该账单类型！");
        }
        url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);

        //创建远程Get请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名,执行请求,进行验签
        CloseableHttpResponse response = wxHttpClient.execute(httpGet);

        //处理响应
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 申请账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请帐单返回结果 = " + bodyAsString);
            }

            //获取账单下载地址
            Gson gson = new Gson();
            Map<String,String> paramsMap = gson.fromJson(bodyAsString, HashMap.class);
            String downloadUrl = paramsMap.get("download_url");

            return downloadUrl;
        } finally {
            response.close();
        }
    }

    /**
     * 下载账单
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @Override
    public String downloadBill(String billDate, String type) throws Exception {

        log.warn("下载账单接口调用 {}, {}", billDate, type);

        //获取下载的url地址
        String downloadUrl = this.queryBill(billDate, type);

        //创建远程请求对象HttpGet
        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.setHeader("Accept", "application/json");

        //完成签名,执行请求,无须验签签名
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);

        //处理响应
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//响应码
            if (statusCode == 200) {
                log.info("成功, 下载账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载帐单返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } finally {
            response.close();
        }
    }

    /**
         * 用户取消订单
         * @param orderNo
         * @throws Exception
         */
        @Override
        public void cancleOrder(String orderNo) throws Exception {

            //调用微信支付的关单接口
            this.wxCloseOrder(orderNo);

            //更新商户系统的订单状态
            orderInfoService.updateOrderStatusByOrderno(orderNo, OrderStatus.CANCEL.getType());
        }
    /**
     * 调用微信的关单接口关闭订单
     * @param orderNo
     * @throws Exception
     */
    private void wxCloseOrder(String orderNo) throws Exception {

        log.info("关单接口的调用，订单号 ===> {}", orderNo);

        //组装url,创建远程请求对象
        String url = WxApiType.CLOSE_ORDER_BY_NO.getType();
        url = wxPayConfig.getDomain().concat(url);
        url = String.format(url, orderNo);  //用orderNo替换url中的占位符
        HttpPost httpPost = new HttpPost(url);

        //组装请求体json
        Gson gson = new Gson();
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("mchid", wxPayConfig.getMchId());
        String jsonParams = gson.toJson(hashMap);
        log.info("请求参数===>{}",jsonParams);

        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpPost);

        //处理返回值
        try {
            int statusCode = response.getStatusLine().getStatusCode();//相应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功204");
            } else {
                log.info("Native下单失败,,响应码 = " + statusCode);
                throw new IOException("request failed");
            }

        } finally {
            response.close();
        }
    }

    /**
     * 对称解密
     * @param bodyMap
     * @return
     * @throws GeneralSecurityException
     */
    private String decryptFromResource(HashMap<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("处理密文");
        //通知数据
        Map<String,String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //附加数据
        String associated_data = resourceMap.get("associated_data");
        //随机串
        String nonce =  resourceMap.get("nonce");

        //数据解密
        log.info("数据密文===>{}",ciphertext);
        AesUtil aesUtil =
                new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);
        log.info("数据明文===>{}",plainText);
        return plainText;
    }
}
